# !/usr/bin/env python3
# coding=utf8
"""
"""


import argparse
import copy
import datetime
import decimal
import enum
import json
import os
import pandas
import re
import struct
from typing import Any, Dict, List, Set, Tuple, Type, Optional, Union, Callable, Sequence


class Interval(enum.Enum):
    """
    见 vnpy/vnpy/trader/constant.py 的 Interval 枚举值 (有DELETE,无INSERT,无UPDATE[有删除部分值,无增加新的值,无修改既定值])
    """
    MINUTE = "1m"
    DAILY = "d"
    MINUTE05 = "5m"


class Exchange(enum.Enum):
    """
    见 vnpy/vnpy/trader/constant.py 的 Exchange 枚举值 (有DELETE,无INSERT,无UPDATE)
    """
    # Chinese
    CFFEX = "CFFEX"         # China Financial Futures Exchange
    SHFE = "SHFE"           # Shanghai Futures Exchange
    CZCE = "CZCE"           # Zhengzhou Commodity Exchange
    DCE = "DCE"             # Dalian Commodity Exchange
    INE = "INE"             # Shanghai International Energy Exchange
    GFEX = "GFEX"           # Guangzhou Futures Exchange
    SSE = "SSE"             # Shanghai Stock Exchange
    SZSE = "SZSE"           # Shenzhen Stock Exchange

    # Global
    SEHK = "SEHK"           # Stock Exchange of Hong Kong
    HKFE = "HKFE"           # Hong Kong Futures Exchange


class Suffix(enum.Enum):
    """
    通达信盘后数据文件的后缀
    """
    LC1 = ".lc1"
    LC5 = ".lc5"
    DAY = ".day"


def output(msg) -> None:
    """
    文件 vnpy_ctastrategy/vnpy_ctastrategy/backtesting.py 的类 BacktestingEngine 里面有 output 函数,
    说明这种写法在这个项目中是可以接受的,
    """
    print(f"{datetime.datetime.now()}\t{msg}")


def guess_filename(
        exchange: str,
        symbol: str,
        interval: str,
        basepath: str,
        output: Callable = print,
) -> Optional[str]:
    """"""
    exchangeEnum: Exchange = Exchange._value2member_map_[exchange]
    intervalEnum: Interval = Interval._value2member_map_[interval]
    return TdxFileMapper.guess_filename_2(
        exchange=exchangeEnum,
        symbol=symbol,
        interval=intervalEnum,
        basepath=basepath,
        output=output,
    )


def get_tdx_price(
    exchange: str,
    symbol: str,
    interval: str,
    start: datetime.datetime,
    end: datetime.datetime,
    start_business_day: datetime.datetime,
    end_business_day: datetime.datetime,
    basepath: str,
    days: List[datetime.datetime],
    output: Callable = print,
) -> Optional[pandas.DataFrame]:
    """
    exchange          : 见 vnpy/vnpy/trader/constant.py 的 Exchange 枚举值
    symbol            : 略
    interval          : 见 vnpy/vnpy/trader/constant.py 的 Interval 枚举值
    start             : 开始时间(含?)
    end               : 结束时间(含?)
    start_business_day: 业务日期的开始时间(含?)
    end_business_day  : 业务日期的结束时间(含?)
    basepath          : 例如"C:\new_tdx\vipdoc"
    days              : 交易日列表(比如"上证指数/000001"的所有交易日)用于正确标识夜盘数据的"自然日期时间"
    """
    # start 和 start_business_day 不能同时有值,
    assert (not start) != (not start_business_day)

    days_copy: List[datetime.datetime] = copy.deepcopy(days)

    df: Optional[pandas.DataFrame] = None

    if exchange not in Exchange._value2member_map_:
        return df

    if interval not in Interval._value2member_map_:
        return df

    exchangeEnum: Exchange = Exchange._value2member_map_[exchange]
    intervalEnum: Interval = Interval._value2member_map_[interval]

    def replace_tzinfo(dttm: Optional[datetime.datetime], tzinfo: Optional[datetime.tzinfo]) -> Optional[datetime.datetime]:
        """"""
        # TODO: 临时去掉时区
        dttm = dttm.replace(tzinfo=tzinfo) if dttm is not None else dttm
        return dttm

    start = replace_tzinfo(dttm=start, tzinfo=None)
    end = replace_tzinfo(dttm=end, tzinfo=None)
    start_business_day = replace_tzinfo(dttm=start_business_day, tzinfo=None)
    end_business_day = replace_tzinfo(dttm=end_business_day, tzinfo=None)

    tdx_full_path: str = TdxFileMapper.guess_filename_2(
        exchange=exchangeEnum,
        symbol=symbol,
        interval=intervalEnum,
        basepath=basepath,
        output=output,
    )
    resolver_class: Type[line_resolver_base] = TdxFileMapper.guess_resolver_2(
        exchange=exchangeEnum,
        symbol=symbol,
        interval=intervalEnum,
        output=output,
    )

    if tdx_full_path is None:  # 猜测文件名失败
        return df

    resolver_obj = SimpleLineResolver()
    resolver_obj.initialize(filepath=tdx_full_path, resolver_class=resolver_class)

    lines: List[dict] = []

    while True:
        data: Optional[dict] = resolver_obj.fetch()
        if data is None:
            break

        assert isinstance(data["datetime"], datetime.datetime)
        assert isinstance(data["business_day"], datetime.datetime)

        line: dict = {
            "business_day": data["business_day"],
            "dttm": data["datetime"],
            "open": data["open"],
            "high": data["high"],
            "low": data["low"],
            "close": data["close"],
            "volume": data.get("volume", None),
            "turnover": data.get("turnover", None),
            "open_interest": data.get("oi", None)
        }
        assert line["business_day"] == line["dttm"].replace(hour=0, minute=0, second=0, microsecond=0)

        if data["interval"] in (Suffix.LC1.value, Suffix.LC5.value):
            # 周四的夜盘在周三的夜里和周四的凌晨交易, 数据里的时间戳为周四的夜里和周四的凌晨,
            # 周五的夜盘在周四的夜里和周五的凌晨交易, 数据里的时间戳为周五的夜里和周五的凌晨,
            # 周一的夜盘在周五的夜里和周六的凌晨交易, 数据里的时间戳为周一的夜里和周一的凌晨,
            # 如果有放假, 那么不会开启夜盘交易,
            # 比如2020-01-01是周三放假了, [2019-12-31夜里~2020-01-01凌晨]和[2020-01-01夜里~2020-01-02凌晨]没有交易,

            if days_copy and len(days_copy) == len(days):
                # 如果我在这周六的早上查数据, 此时上证指数里的交易日只到这周五, 而数据里的时间戳会标识到下周一的夜里和凌晨,
                # 所以我需要把下周一的交易日也补进去,
                delta_days: int = 3 if days_copy[-1].weekday() == 4 else 1  # 周五的weekday是4
                will_day: datetime.datetime = days_copy[-1] + datetime.timedelta(days=delta_days)
                days_copy.append(will_day)

            dttm: datetime.datetime = line["dttm"]

            if 20 <= dttm.hour:
                # 当前逻辑交易日在16点之前就收盘了, 20点之后的数据属于下一个逻辑交易日的夜盘, 同时它在自然界的日期等于当前逻辑交易日,
                idx: int = days_copy.index(line["business_day"])  # 这条数据的逻辑交易日, 在交易日历的哪个位置,
                day: datetime.datetime = days_copy[idx - 1]  # 这条数据的逻辑交易日, 的上一个逻辑交易日, 就是这条数据在自然界的日期,
                natural_time: datetime.datetime = datetime.datetime.combine(day.date(), dttm.time())  # 这条数据在自然界的时间点,
                line["dttm"] = natural_time

            if dttm.hour <= 3:
                # 因为夜盘是一个连续的时间段, 比如黄金期货的夜盘时间段是[本日的21点00分~次日的02点30分], 所以一旦发现数据的时间戳在0点到3点之间,
                # 就可以: 拿到这条数据的逻辑交易日, 找到上一个逻辑交易日, 计算下一个自然界的日期, 从而计算出这条数据在自然界的时间点,
                idx: int = days_copy.index(line["business_day"])  # 这条数据的逻辑交易日, 在交易日历的哪个位置,
                day: datetime.datetime = days_copy[idx - 1]  # 这条数据的逻辑交易日, 的上一个逻辑交易日,
                day: datetime.datetime = day + datetime.timedelta(days=1)  # 上一个逻辑交易日, 的下一个自然界的日期, 就是这条数据在自然界的日期,
                natural_time: datetime.datetime = datetime.datetime.combine(day.date(), dttm.time())  # 这条数据在自然界的时间点,
                line["dttm"] = natural_time

        if start:
            if end is None:
                is_in_range: bool = start <= line["dttm"]
            else:
                is_in_range: bool = start <= line["dttm"] <= end
        else:
            if end_business_day is None:
                is_in_range: bool = start_business_day <= line["business_day"]
            else:
                is_in_range: bool = start_business_day <= line["business_day"] <= end_business_day

        if not is_in_range:
            continue

        lines.append(line)

    lines.sort(key=lambda x: x["dttm"])
    if lines:
        df: pandas.DataFrame = pandas.DataFrame(data=lines)
        df.set_index(keys=["dttm"], drop=True, inplace=True)

    return df


class TdxFileMapper(object):
    """"""

    BASEPATH: str = r"C:\new_tdx\vipdoc"

    SUFFIX_MAP: Dict[Interval, Suffix] = {Interval.MINUTE: Suffix.LC1, Interval.MINUTE05: Suffix.LC5, Interval.DAILY: Suffix.DAY}

    DIR2_MAP: Dict[Suffix, str] = {Suffix.LC1: "minline", Suffix.LC5: "fzline", Suffix.DAY: "lday"}

    def __init__(self) -> None:
        """"""
        pass

    @classmethod
    def guess_dir1_and_file_prefix(cls, exchange: Exchange, symbol: str) -> Tuple[str, str]:
        """"""
        FILE_PREFIX_MAP: Dict[Exchange, int] = {
            Exchange.CFFEX: 47,
            Exchange.CZCE: 28,
            Exchange.DCE: 29,
            Exchange.GFEX: 66,
            Exchange.INE: 30,
            Exchange.SHFE: 30,
        }

        dir1: Optional[str] = None

        file_prefix: Optional[str] = None

        if exchange == Exchange.SEHK:
            dir1: Optional[str] = "ds"

            if re.match(r"^[A-Z]+$", symbol) is not None:
                # 暂定纯英文字母时表示香港指数
                file_prefix = f"27#"
            elif re.match(r"^0[0-9]{4}$", symbol) is not None:
                file_prefix = f"31#"

        elif exchange == Exchange.SSE:
            file_prefix = dir1 = "sh"

        elif exchange == Exchange.SZSE:
            file_prefix = dir1 = "sz"

        elif exchange in FILE_PREFIX_MAP:
            dir1: Optional[str] = "ds"
            file_prefix = f"{FILE_PREFIX_MAP[exchange]}#"

        assert (dir1 is not None) and (file_prefix is not None)

        return dir1, file_prefix

    @classmethod
    def guess_filename_2(cls, exchange: Exchange, symbol: str, interval: Interval, basepath: str, output: Callable = print) -> str:
        """
        ICL8 中证主连 中金所(示例:IC2303)(vipdoc/ds/minline/47#ICL8.lc1)
        SRL8 白糖主连 郑商所(示例:SR301) (vipdoc/ds/minline/28#SRL8.lc1)
        IL8  铁矿主连 大商所(示例:i2301) (vipdoc/ds/minline/29#IL8.lc1)
        SCL8 原油主连 上能源(示例:sc2301)(vipdoc/ds/minline/30#SCL8.lc1)
        AUL8 黄金主连 上期所(示例:au2301)(vipdoc/ds/minline/30#AUL8.lc1)
        SIL8 工业硅主连 广期所(例:si2505)(vipdoc/ds/minline/66#SIL8.lc1)
        (Please also see Chapter 3 of the Rules of The Stock Exchange of Hong Kong Limited ("SEHK"))
        (Please also see Chapters III & V of the Rules of Hong Kong Futures Exchange Limited ("HKFE"))
        HSI  恒生指数 港交所(示例:HSI)   (vipdoc/ds/minline/27#HSI.lc1)
        """
        tdx_file_path: Optional[str] = None

        suffix: Suffix = cls.SUFFIX_MAP[interval]
        # symbol
        file_prefix: Optional[str] = None
        dir2: str = cls.DIR2_MAP[suffix]
        dir1: Optional[str] = None

        dir1, file_prefix = cls.guess_dir1_and_file_prefix(exchange=exchange, symbol=symbol)

        if basepath and dir1 and dir2 and file_prefix and symbol and suffix:
            tdx_base_name = f"{file_prefix}{symbol}{suffix.value}"
            tdx_file_path = os.path.join(basepath, dir1, dir2, tdx_base_name)

        return tdx_file_path

    @classmethod
    def guess_resolver_2(cls, exchange: Exchange, symbol: str, interval: Interval, output: Callable = print) -> type:
        """"""
        if exchange == Exchange.SSE:
            return cls._resolver_SSE(exchange=exchange, symbol=symbol, interval=interval)
        elif exchange == Exchange.SZSE:
            return cls._resolver_SZSE(exchange=exchange, symbol=symbol, interval=interval)
        elif exchange == Exchange.SEHK:
            return cls._resolver_SEHK(exchange=exchange, symbol=symbol, interval=interval)
        elif exchange in (Exchange.CFFEX, Exchange.CZCE, Exchange.DCE, Exchange.GFEX, Exchange.INE, Exchange.SHFE):
            return cls._resolver_CFFEX_CZCE_DCE_GFEX_INE_SHFE(exchange=exchange, symbol=symbol, interval=interval)
        else:
            raise RuntimeError("unknown exchange=[{}]".format(exchange))

    @classmethod
    def _resolver_SSE(cls, exchange: Exchange, symbol: str, interval: Interval) -> type:
        """ 返回值不可为None """
        assert exchange == Exchange.SSE

        line_resolver_class: Type[line_resolver_base] = None

        suffix: Suffix = cls.SUFFIX_MAP[interval]

        if suffix == Suffix.DAY:
            if False:
                pass
            elif symbol.startswith('6'):
                line_resolver_class = line_resolver_day_stock
            elif symbol.startswith('5'):
                line_resolver_class = line_resolver_day_fund
            elif symbol.startswith("000"):
                line_resolver_class = line_resolver_day_index
            else:
                raise RuntimeError("unknown symbol=[{}]".format(symbol))
        elif suffix in (Suffix.LC1, Suffix.LC5):
            if symbol.startswith('5') or symbol.startswith('6') or symbol.startswith("000"):
                line_resolver_class = line_resolver_lc_stock_fund
            else:
                raise RuntimeError("unknown symbol=[{}]".format(symbol))
        else:
            raise RuntimeError("unknown suffix=[{}]".format(suffix))

        assert line_resolver_class is not None

        return line_resolver_class

    @classmethod
    def _resolver_SZSE(cls, exchange: Exchange, symbol: str, interval: Interval) -> type:
        """"""
        assert exchange == Exchange.SZSE

        line_resolver_class: Type[line_resolver_base] = None

        suffix: Suffix = cls.SUFFIX_MAP[interval]

        if suffix == Suffix.DAY:
            if False:
                pass
            elif symbol.startswith('0'):
                line_resolver_class = line_resolver_day_stock
            elif symbol.startswith('159'):
                line_resolver_class = line_resolver_day_fund
            elif symbol.startswith("399"):
                line_resolver_class = line_resolver_day_index
            else:
                raise RuntimeError("unknown symbol=[{}]".format(symbol))
        elif suffix in (Suffix.LC1, Suffix.LC5):
            if symbol.startswith('0') or symbol.startswith('159') or symbol.startswith("399"):
                line_resolver_class = line_resolver_lc_stock_fund
            else:
                raise RuntimeError("unknown symbol=[{}]".format(symbol))
        else:
            raise RuntimeError("unknown suffix=[{}]".format(suffix))

        assert line_resolver_class is not None

        return line_resolver_class

    @classmethod
    def _resolver_CFFEX_CZCE_DCE_GFEX_INE_SHFE(cls, exchange: Exchange, symbol: str, interval: Interval) -> type:
        """"""
        line_resolver_class: Type[line_resolver_base] = None

        suffix: Suffix = cls.SUFFIX_MAP[interval]

        if suffix in (Suffix.DAY,):
            line_resolver_class = line_resolver_day_futures
        elif suffix in (Suffix.LC1, Suffix.LC5):
            line_resolver_class = line_resolver_lc_futures
        else:
            raise RuntimeError("unknown suffix=[{}]".format(suffix))

        assert line_resolver_class is not None

        return line_resolver_class

    @classmethod
    def _resolver_SEHK(cls, exchange: Exchange, symbol: str, interval: Interval) -> type:
        """"""
        line_resolver_class: Type[line_resolver_base] = None

        suffix: Suffix = cls.SUFFIX_MAP[interval]

        if suffix in (Suffix.DAY,):
            line_resolver_class = line_resolver_day_futures
        elif suffix in (Suffix.LC1, Suffix.LC5):
            line_resolver_class = line_resolver_lc_sehk_00700
        else:
            raise RuntimeError("unknown suffix=[{}]".format(suffix))

        assert line_resolver_class is not None

        return line_resolver_class


def dict2line(src: dict, id, symbol, exchange) -> str:
    """[datetime,open,high,low,close,volume,open_interest]"""
    tmp = {}
    tmp['id'] = id
    tmp['symbol'] = symbol
    tmp['exchange'] = exchange
    tmp['datetime'] = src['datetime']
    tmp['interval'] = 'd'  # DAILY = "d"
    tmp['volume'] = src['volume']
    tmp['open_interest'] = src['position']
    tmp['open_price'] = src['open']
    tmp['high_price'] = src['high']
    tmp['low_price'] = src['low']
    tmp['close_price'] = src['close']
    line = '{datetime},{open_price},{high_price},{low_price},{close_price},{volume},{open_interest}'.format(**tmp)
    return line


def json_dumps(src) -> str:
    def json_handler(obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime("%Y-%m-%d %H:%M:%S.%f")
        if isinstance(obj, datetime.date):
            return obj.strftime("%Y-%m-%d")
        if isinstance(obj, bytes):
            return obj.__repr__()
        if isinstance(obj, decimal.Decimal):
            return float(obj)
        raise TypeError(obj.__repr__() + " is not JSON serializable")

    return json.dumps(src, sort_keys=True, indent=2, default=json_handler)


class WrapBase(object):
    '''Base Wrapper; 包装器的接口; 所有包装器的基类;'''

    def __init__(self, *args, **kwargs):
        pass

    @property
    def value(self):
        return self.call()

    def call(self):
        raise NotImplementedError('not_implemented_error')


class WrapPlain(WrapBase):
    '''Plain Wrapper'''

    def __init__(self, value, *args, **kwargs):
        super(WrapPlain, self).__init__(value, *args, **kwargs)
        self._value = value

    def call(self):
        return self._value


class WrapObj(WrapBase):
    '''Object Wrapper'''

    def __init__(self, obj, name: str, *args, **kwargs):
        super(WrapObj, self).__init__(obj, name, *args, **kwargs)
        self._obj = obj
        self._name = name

    def call(self):
        return getattr(self._obj, self._name)


class field_resolver(object):
    '''字段解析器'''

    def __init__(self, name: str, bintype: str, begindx: int, endindx: int, check: bool = True, ndigits: int = None, export: str = None, *args, **kwargs):
        '''
        ndigits: 当 bintype 为 float/double 时, ndigits 有效; 此时, 会将数字四舍五入到给定的精度;
        export : 如果有值, 就往字典中存入一对 key_value, 其中, key = self._export, value = self._value;
        '''
        self._name: str = name
        self._value = None
        self._bintype: str = bintype
        self._begindx: int = begindx
        self._endindx: int = endindx
        self._check: bool = check
        self._ndigits: int = ndigits
        self._export: str = export

    def calc(self, content: bytes):
        """"""
        src = content[self._begindx:self._endindx + 1]
        if False:
            pass
        elif self._bintype == 'bytes':
            self._value = field_resolver.bytes_to_bytes(src, self._check)[0]
        elif self._bintype == 'char':
            self._value = field_resolver.bytes_to_char(src, self._check)[0]
        elif self._bintype == 'short':
            self._value = field_resolver.bytes_to_short(src, self._check)[0]
        elif self._bintype == 'unsignedshort':
            self._value = field_resolver.bytes_to_unsignedshort(src, self._check)[0]  # noqa E501
        elif self._bintype == 'int':
            self._value = field_resolver.bytes_to_int(src, self._check)[0]
        elif self._bintype == 'longlong':
            self._value = field_resolver.bytes_to_longlong(src, self._check)[0]
        elif self._bintype == 'float':
            self._value = field_resolver.bytes_to_float(src, self._check)[0]
            if self._ndigits is not None:
                self._value = round(decimal.Decimal(self._value), self._ndigits)  # noqa E501
        elif self._bintype == 'double':
            self._value = field_resolver.bytes_to_double(src, self._check)[0]
            if self._ndigits is not None:
                self._value = round(decimal.Decimal(self._value), self._ndigits)  # noqa E501
        else:
            raise Exception('unknown bintype=[{}]'.format(self._bintype))

    @staticmethod
    def bytes_to_bytes(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes)
        return (src, )

    @staticmethod
    def bytes_to_char(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes) and len(src) == 1
        return struct.unpack("c", src)

    @staticmethod
    def bytes_to_short(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes) and len(src) == 2
        return struct.unpack("h", src)

    @staticmethod
    def bytes_to_unsignedshort(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes) and len(src) == 2
        return struct.unpack("H", src)

    @staticmethod
    def bytes_to_int(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes) and len(src) == 4
        return struct.unpack("i", src)

    @staticmethod
    def bytes_to_longlong(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes) and len(src) == 8
        return struct.unpack("q", src)

    @staticmethod
    def bytes_to_float(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes) and len(src) == 4
        return struct.unpack("f", src)

    @staticmethod
    def bytes_to_double(src: bytes, check=True):
        """"""
        if check:
            assert isinstance(src, bytes) and len(src) == 8
        return struct.unpack("d", src)


class field_conv(object):
    """field converter; 字段转换器;"""

    def __init__(self, name: str, func: Callable, export: str = None, *args, **kwargs):
        """
        export : 如果有值, 就往字典中存入一对 key_value, 其中, key = self._export, value = self._value;
        """
        self._name: str = name
        self._value: object = None
        self._func: Callable = func
        self._export: str = export
        self._args: tuple = args
        self._kwargs: dict = kwargs

    def calc(self):
        """"""
        self._value = self._func(*self._args, **self._kwargs)

    @staticmethod
    def shrink_int_to_float(arg1: WrapBase, arg2: WrapBase, *args, **kwargs) -> float:
        """将int缩小成double"""
        src: int = arg1.value  # int, 例如 517819
        multiple: int = arg2.value  # int, 例如 100
        assert isinstance(src, int) and isinstance(multiple, int)
        return src / multiple  # float, 例如 5178.19

    @staticmethod
    def enlarge_int_to_float(arg1: WrapBase, arg2: WrapBase, *args, **kwargs) -> float:
        """将int放大成double"""
        src: int = arg1.value  # int, 例如 30556901
        multiple: int = arg2.value  # int, 例如 100
        assert isinstance(src, int) and isinstance(multiple, int)
        return src * multiple * 1.0  # float, 例如 3055690100.0

    @classmethod
    def zoom_number_to_float(cls, arg1: WrapBase, arg2: WrapBase, *args, **kwargs) -> float:
        """
        将数字缩放成double
        10010 * 0.01 => 100.10
        100.86 * 100 => 10086
        """
        src: Union[int, float] = arg1.value  # int 或 float, 例如 10010
        multiple: Union[int, float] = arg2.value  # int 或 float, 例如 0.01
        assert isinstance(src, (int, float)) and isinstance(multiple, (int, float))
        return src * multiple * 1.0  # float, 例如 100.10

    @staticmethod
    def tdx_field_to_yyyymmdd(arg1: WrapBase, *args, **kwargs) -> int:
        """"""
        d: int = arg1.value  # date_num
        assert isinstance(d, int)
        year: int = d // 2048 + 2004
        month: int = d % 2048 // 100
        day: int = d % 2048 % 100
        return year * 10000 + month * 100 + day

    @staticmethod
    def tdx_field_to_hhmm(arg1: WrapBase, *args, **kwargs) -> int:
        """"""
        t: int = arg1.value  # time_num
        assert isinstance(t, int)
        hour: int = t // 60
        minute: int = t % 60
        return hour * 100 + minute

    @staticmethod
    def tdx_field_to_date_tm0(arg1: WrapBase, *args, **kwargs) -> datetime.datetime:
        """"""
        d: int = arg1.value  # date_num
        assert isinstance(d, int)
        year: int = d // 2048 + 2004
        month: int = d % 2048 // 100
        day: int = d % 2048 % 100
        return datetime.datetime(year, month, day)

    @staticmethod
    def tdx_field_to_datetime(arg1: WrapBase, arg2: WrapBase, *args, **kwargs) -> datetime.datetime:
        """"""
        d: int = arg1.value  # date_num
        t: int = arg2.value  # time_num
        assert isinstance(d, int) and isinstance(t, int)
        year: int = d // 2048 + 2004
        month: int = d % 2048 // 100
        day: int = d % 2048 % 100
        hour: int = t // 60
        minute: int = t % 60
        return datetime.datetime(year, month, day, hour, minute)

    @staticmethod
    def yyyymmdd_to_datetime(arg1: WrapBase, *args, **kwargs) -> datetime.datetime:
        """"""
        d: int = arg1.value  # date_num, 例如 20060102
        assert isinstance(d, int)
        year: int = d // 10000
        month: int = d % 10000 // 100
        day: int = d % 100
        return datetime.datetime(year, month, day)

    @staticmethod
    def yyyymmdd_to_date(arg1: WrapBase, *args, **kwargs) -> datetime.date:
        """"""
        d: int = arg1.value  # date_num, 例如 20060102
        assert isinstance(d, int)
        year: int = d // 10000
        month: int = d % 10000 // 100
        day: int = d % 100
        return datetime.date(year, month, day)


class line_resolver_base(object):
    """ 行解析器基类, 解析每一行数据的解析器的基类 """

    def __init__(self, *args, **kwargs):
        """"""
        self._r_list: List[field_resolver] = []
        self._c_list: List[field_conv] = []
        self._filepath: str = ""  # 文件路径
        self._filename: str = ""  # 文件名
        self._interval: str = ""  # 文件名后缀(filename suffix);(1分钟线,5分钟线,1日线,)
        self._filedata: bytes = b""  # 文件内容(二进制)
        self._datindex: int = 0  # 当前位于第几行(一行有32字节)
        self._datcount: int = 0  # 总共有多少行

    def initialize(self, filepath: str) -> None:
        """ apr 有 apr_initialize 和 apr_terminate 我很喜欢这俩函数, 所以就写进来了; """
        all_content: bytes = b""
        try:
            with open(file=filepath, mode="rb") as f:
                all_content: bytes = f.read()
        except FileNotFoundError as ex:
            output(ex)

        assert len(all_content) % 32 == 0

        self._filepath: str = filepath
        self._filename: str = os.path.basename(self._filepath)
        self._interval: str = os.path.splitext(self._filename)[1]
        self._filedata: bytes = all_content
        self._datindex: int = 0
        self._datcount: int = len(all_content) // 32

    def fetch(self) -> Optional[dict]:
        """ 若返回值为 None, 则数据已取完 """
        data: Optional[dict] = None

        if self._datindex < self._datcount:
            idx: int = self._datindex
            cur_content: bytes = self._filedata[idx * 32:(idx + 1) * 32]
            data: dict = self.__calc_data(cur_content)
            self._datindex += 1

        return data

    def __calc_data(self, content: bytes) -> dict:
        """"""
        assert isinstance(content, bytes) and len(content) == 32

        line: dict = {}

        # 每一行结果里面都携带文件名和文件后缀, 便于使用, 也便于排查问题,
        if True:
            line["interval"] = self._interval  # 文件后缀(它标识了数据是1分钟线/5分钟线/日线/等)
        if True:
            line["filename"] = self._filename  # 文件名

        for item in self._r_list:  # "字段解析器"
            item.calc(content)  # "字段解析器"根据这一行数据, 计算出这一行数据的某个字段的结果, 结果存储在"字段解析器"里,
            setattr(self, item._name, item._value)  # 将"字段解析器"里的结果存储到"行解析器"里,
            if item._export:
                line[item._export] = item._value  # "字段解析器"里的结果就可以被外部使用(一般来说就是最终结果),

        for item in self._c_list:  # "字段转换器"在构造的时候接收了一些参数, 其中就有"WrapBase"对象, 它里面保存了"行解析器"的对象实例,
            item.calc()  # 因为在构造的时候就保存了"行解析器"的对象实例, 所以没有入参就可以直接计算了, 结果存储在"字段转换器"里,
            setattr(self, item._name, item._value)  # 将"字段转换器"里的结果存储到"行解析器"里,
            if item._export:
                line[item._export] = item._value  # "字段转换器"里的结果就可以被外部使用(一般来说就是最终结果),

        return line


class SimpleLineResolver(object):
    """"""

    def __init__(self, *args, **kwargs) -> None:
        """"""
        self._filepath: str = ""
        self._resolver: line_resolver_base = None

    def initialize(self, filepath: str, resolver_class: Type[line_resolver_base]) -> None:
        """"""
        self._filepath: str = filepath
        self._resolver: line_resolver_base = resolver_class()
        self._resolver.initialize(filepath)

    def fetch(self) -> Optional[dict]:
        """"""
        return self._resolver.fetch()


class line_resolver_day_stock(line_resolver_base):
    """
    日线, 股票,
    ./vipdoc/sh/lday/sh600000.day
    ./vipdoc/sz/lday/sz000001.day
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_day_stock, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'int', 0, 3),
            field_resolver('_r_open', 'int', 4, 7, ndigits=4),
            field_resolver('_r_high', 'int', 8, 11, ndigits=4),
            field_resolver('_r_low', 'int', 12, 15, ndigits=4),
            field_resolver('_r_close', 'int', 16, 19, ndigits=4),
            field_resolver('_r_turnover', 'float', 20, 23, ndigits=4, export='turnover'),
            field_resolver('_r_volume', 'int', 24, 27, export='volume'),
            field_resolver('_r_reserved', 'int', 28, 31, export='reserved')
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='datetime'),  # noqa E501
            field_conv('_c_open', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_open'), arg2=WrapPlain(100), export='open'),  # noqa E501
            field_conv('_c_high', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_high'), arg2=WrapPlain(100), export='high'),  # noqa E501
            field_conv('_c_low', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_low'), arg2=WrapPlain(100), export='low'),  # noqa E501
            field_conv('_c_close', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_close'), arg2=WrapPlain(100), export='close')  # noqa E501
        ]


class line_resolver_day_fund(line_resolver_base):
    """
    日线, 基金,
    ./vipdoc/sh/lday/sh510300.day
    ./vipdoc/sz/lday/sz159919.day
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_day_fund, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'int', 0, 3),
            field_resolver('_r_open', 'int', 4, 7, ndigits=4),
            field_resolver('_r_high', 'int', 8, 11, ndigits=4),
            field_resolver('_r_low', 'int', 12, 15, ndigits=4),
            field_resolver('_r_close', 'int', 16, 19, ndigits=4),
            field_resolver('_r_turnover', 'float', 20, 23, ndigits=4, export='turnover'),
            field_resolver('_r_volume', 'int', 24, 27, export='volume'),
            field_resolver('_r_reserved', 'int', 28, 31, export='reserved')
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='datetime'),  # noqa E501
            field_conv('_c_open', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_open'), arg2=WrapPlain(1000), export='open'),  # noqa E501
            field_conv('_c_high', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_high'), arg2=WrapPlain(1000), export='high'),  # noqa E501
            field_conv('_c_low', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_low'), arg2=WrapPlain(1000), export='low'),  # noqa E501
            field_conv('_c_close', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_close'), arg2=WrapPlain(1000), export='close')  # noqa E501
        ]


class line_resolver_day_index(line_resolver_base):
    """
    日线, 指数,
    ./vipdoc/sh/lday/sh000300.day
    ./vipdoc/sh/lday/sh000905.day
    ./vipdoc/sz/lday/sz399001.day
    ./vipdoc/sz/lday/sz399006.day
    ./vipdoc/sz/lday/sz399300.day
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_day_index, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'int', 0, 3),
            field_resolver('_r_open', 'int', 4, 7, ndigits=4),
            field_resolver('_r_high', 'int', 8, 11, ndigits=4),
            field_resolver('_r_low', 'int', 12, 15, ndigits=4),
            field_resolver('_r_close', 'int', 16, 19, ndigits=4),
            field_resolver('_r_turnover', 'float', 20, 23, ndigits=4, export="turnover"),
            field_resolver('_r_volume', 'int', 24, 27, export="volume"),
            field_resolver('_r_reserved', 'int', 28, 31, export='reserved'),
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='datetime'),  # noqa E501
            field_conv('_c_open', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_open'), arg2=WrapPlain(100), export='open'),  # noqa E501
            field_conv('_c_high', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_high'), arg2=WrapPlain(100), export='high'),  # noqa E501
            field_conv('_c_low', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_low'), arg2=WrapPlain(100), export='low'),  # noqa E501
            field_conv('_c_close', field_conv.shrink_int_to_float, arg1=WrapObj(self, '_r_close'), arg2=WrapPlain(100), export='close'),  # noqa E501
        ]


class line_resolver_day_futures(line_resolver_base):
    """
    日线,期货
    ./vipdoc/ds/lday/47#IFL0.day
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_day_futures, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'int', 0, 3),
            field_resolver('_r_open', 'float', 4, 7, ndigits=3, export='open'),
            field_resolver('_r_high', 'float', 8, 11, ndigits=3, export='high'),
            field_resolver('_r_low', 'float', 12, 15, ndigits=3, export='low'),
            field_resolver('_r_close', 'float', 16, 19, ndigits=3, export='close'),
            field_resolver('_r_oi', 'int', 20, 23, ndigits=3, export='oi'),  # open_interest
            field_resolver('_r_volume', 'int', 24, 27, export='volume'),
            field_resolver('_r_settle', 'float', 28, 31, ndigits=3, export='settle')
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.yyyymmdd_to_datetime, arg1=WrapObj(self, '_r_date'), export='datetime'),  # noqa E501
        ]


class line_resolver_lc_stock_fund(line_resolver_base):
    """
    分钟线,股票,基金,
    ./vipdoc/sh/minline/sh510300.lc1
    ./vipdoc/sh/minline/sh600000.lc1
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_lc_stock_fund, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'unsignedshort', 0, 1),
            field_resolver('_r_time', 'short', 2, 3),
            field_resolver('_r_open', 'float', 4, 7, ndigits=4, export='open'),
            field_resolver('_r_high', 'float', 8, 11, ndigits=4, export='high'),
            field_resolver('_r_low', 'float', 12, 15, ndigits=4, export='low'),
            field_resolver('_r_close', 'float', 16, 19, ndigits=4, export='close'),
            field_resolver('_r_turnover', 'float', 20, 23, ndigits=4, export='turnover'),
            field_resolver('_r_volume', 'int', 24, 27, export='volume'),
            field_resolver('_r_reserved', 'int', 28, 31, export='reserved')
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.tdx_field_to_date_tm0, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.tdx_field_to_datetime, arg1=WrapObj(self, '_r_date'), arg2=WrapObj(self, '_r_time'), export='datetime'),  # noqa E501
        ]


class line_resolver_lc_futures(line_resolver_base):
    """
    分钟线,期货,比如通达信的(IFL8)(沪深主连)
    ./vipdoc/ds/minline/47#IFL0.lc1
    ./vipdoc/ds/fzline/47#IFL0.lc5
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_lc_futures, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'unsignedshort', 0, 1),
            field_resolver('_r_time', 'short', 2, 3),
            field_resolver('_r_open', 'float', 4, 7, ndigits=3, export='open'),
            field_resolver('_r_high', 'float', 8, 11, ndigits=3, export='high'),
            field_resolver('_r_low', 'float', 12, 15, ndigits=3, export='low'),
            field_resolver('_r_close', 'float', 16, 19, ndigits=3, export='close'),
            field_resolver('_r_oi', 'int', 20, 23, export='oi'),
            field_resolver('_r_volume', 'int', 24, 27, export='volume'),
            field_resolver('_r_reserved', 'int', 28, 31, export='reserved')
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.tdx_field_to_date_tm0, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.tdx_field_to_datetime, arg1=WrapObj(self, '_r_date'), arg2=WrapObj(self, '_r_time'), export='datetime'),  # noqa E501
        ]


class line_resolver_lc_sehk_00700(line_resolver_base):
    """
    分钟线,香港,
    filepath: str = r"C:/new_tdx/vipdoc/ds/minline/31#00700.lc1"  # 腾讯控股
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_lc_sehk_00700, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'unsignedshort', 0, 1),
            field_resolver('_r_time', 'short', 2, 3),
            field_resolver('_r_open', 'float', 4, 7, ndigits=3, export='open'),
            field_resolver('_r_high', 'float', 8, 11, ndigits=3, export='high'),
            field_resolver('_r_low', 'float', 12, 15, ndigits=3, export='low'),
            field_resolver('_r_close', 'float', 16, 19, ndigits=3, export='close'),
            field_resolver('_r_turnover', 'float', 20, 23, ndigits=3, export='turnover'),  # 不用变
            field_resolver('_r_volume', 'int', 24, 27),  # 乘100
            field_resolver('_r_reserved', 'int', 28, 31, export='reserved')
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.tdx_field_to_date_tm0, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.tdx_field_to_datetime, arg1=WrapObj(self, '_r_date'), arg2=WrapObj(self, '_r_time'), export='datetime'),  # noqa E501
            field_conv('_c_volume', field_conv.enlarge_int_to_float, arg1=WrapObj(self, '_r_volume'), arg2=WrapPlain(100), export='volume'),  # noqa E501
        ]


class line_resolver_lc_sehk_hsi(line_resolver_base):
    """
    分钟线,香港,
    filepath: str = r"C:/new_tdx/vipdoc/ds/minline/27#HSI.lc1"  # 恒生指数
    """

    def __init__(self, *args, **kwargs):
        """"""
        super(line_resolver_lc_sehk_hsi, self).__init__(*args, **kwargs)
        self._r_list: List[field_resolver] = [
            field_resolver('_r_date', 'unsignedshort', 0, 1),
            field_resolver('_r_time', 'short', 2, 3),
            field_resolver('_r_open', 'float', 4, 7, ndigits=3, export='open'),
            field_resolver('_r_high', 'float', 8, 11, ndigits=3, export='high'),
            field_resolver('_r_low', 'float', 12, 15, ndigits=3, export='low'),
            field_resolver('_r_close', 'float', 16, 19, ndigits=3, export='close'),
            field_resolver('_r_turnover', 'float', 20, 23, ndigits=3),  # 乘100W
            field_resolver('_r_reserved1', 'int', 24, 27, export='reserved1'),
            field_resolver('_r_reserved2', 'int', 28, 31, export='reserved2')
        ]
        self._c_list: List[field_conv] = [
            field_conv('_c_business_day', field_conv.tdx_field_to_date_tm0, arg1=WrapObj(self, '_r_date'), export='business_day'),  # noqa E501
            field_conv('_c_datetime', field_conv.tdx_field_to_datetime, arg1=WrapObj(self, '_r_date'), arg2=WrapObj(self, '_r_time'), export='datetime'),  # noqa E501
            field_conv('_c_turnover', field_conv.zoom_number_to_float, arg1=WrapObj(self, '_r_turnover'), arg2=WrapPlain(1000000), export='turnover'),  # noqa E501
        ]


if __name__ == '__main__':
    parse = argparse.ArgumentParser()
    parse.add_argument('-f', '--file', type=str)
    parse.add_argument('-r', '--resolver', type=str)
    args = parse.parse_args()

    basepath = TdxFileMapper.BASEPATH

    def get_000001_days():
        df: pandas.DataFrame = get_tdx_price(
            exchange=Exchange.SSE.value,
            symbol="000001",
            interval=Interval.DAILY.value,
            start=datetime.datetime(year=1990, month=12, day=19),
            end=datetime.datetime.now(),
            start_business_day=None,
            end_business_day=None,
            basepath=basepath,
            days=[],
            output=print,
        )
        _000001_days: List[datetime.datetime] = df.index.to_pydatetime().tolist()
        return _000001_days

    _000001_days = get_000001_days()

    beg: datetime.datetime = datetime.datetime(year=2023, month=10, day=17, hour=2, minute=16)
    end: datetime.datetime = datetime.datetime(year=2023, month=10, day=18, hour=2, minute=20)
    df: pandas.DataFrame = get_tdx_price(
        exchange=Exchange.SHFE.value,
        symbol="AUL8",
        interval=Interval.MINUTE.value,
        start=beg,
        end=end,
        start_business_day=None,
        end_business_day=None,
        basepath=basepath,
        days=_000001_days,
        output=print,
    )

    pandas.set_option('display.max_rows', None)  # 显示所有行
    print(df)

# 通达信日线day文件格式详解（含港股）_二粒米的博客-CSDN博客
# https://blog.csdn.net/weixin_41494909/article/details/91410664
